package ElectricCraft.model;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.util.ArrayList;
import ElectricCraft.misc.Rectangle;

/**
 * ChipModel class contains chip board elements.
 */
public final class ChipModel {
    public static final int DEFAULT_MODEL_SIZE = 65;

    public String name;
	public final ChipElement[][] elements;

    public final ArrayList<ChipModel> models = new ArrayList<ChipModel>();

    public final ArrayList<Boolean> inputs = new ArrayList<Boolean>();
    public final ArrayList<Boolean> outputs = new ArrayList<Boolean>();

    public ModelState state = null;

	public ChipModel(String name) {
        this.name = name;

		elements = new ChipElement[DEFAULT_MODEL_SIZE][];
		for (int k = 0; k < DEFAULT_MODEL_SIZE; ++k) {
			elements[k] = new ChipElement[DEFAULT_MODEL_SIZE];
		}
	}

    public ChipModel(ChipModel model) {
        elements = new ChipElement[DEFAULT_MODEL_SIZE][];
        for (int k = 0; k < DEFAULT_MODEL_SIZE; ++k) {
            elements[k] = new ChipElement[DEFAULT_MODEL_SIZE];
        }

        model.copy(this);
    }

    public void copy(ChipModel dst) {
        dst.name = name;

        for (int x = 0; x < elements.length; ++x) {
            for (int y = 0; y < elements.length; ++y) {
                ChipElement elem = elements[x][y];
                if (elem == null) {
                    dst.elements[x][y] = null;
                    continue;
                }

                dst.elements[x][y] = elem.duplicate();
            }
        }

        dst.models.clear();
        for (ChipModel model : models) {
            dst.models.add(new ChipModel(model));
        }

        dst.inputs.clear();
        dst.inputs.addAll(inputs);

        dst.outputs.clear();
        dst.outputs.addAll(outputs);
    }

    public void setModelState(ModelState state) {
        this.state = state;

        for (ChipModel model : models) {
            model.setModelState(state);
        }
    }

	public int acquireInput() {
		for (int k = 0; k < inputs.size(); ++k) {
			if (!inputs.get(k)) {
				inputs.set(k, true);
				return k + 1;
			}
		}

		inputs.add(true);
		return inputs.size();
	}

	public int acquireOutput() {
		for (int k = 0; k < outputs.size(); ++k) {
			if (!outputs.get(k)) {
				outputs.set(k, true);
				return k + 1;
			}
		}

		outputs.add(true);
		return outputs.size();
	}

	public void releaseInput(int index) {
		inputs.set(index - 1, false);
	}

	public void releaseOutput(int index) {
		outputs.set(index - 1, false);
	}

    public void removeModel(int index) {
        models.remove(index);

        for (ChipElement [] row : elements) {
            for (ChipElement elem : row) {
                if (elem == null || elem.type != ChipElement.ELEMENT_CHIP_PART) {
                    continue;
                }

                assert elem.itemType != index;

                if (elem.itemType > index) {
                    --elem.itemType;
                }
            }
        }
    }

    public Rectangle calcElementsRange() {
        Rectangle range = new Rectangle();

        for (int x = 0; x < elements.length; ++x) {
            for (int y = 0; y < elements.length; ++y) {
                ChipElement elem = elements[x][y];
                if (elem == null) {
                    continue;
                }

                if (range.width == 0 || range.height == 0) {
                    range.setBounds(x, y, 1, 1);
                    continue;
                }

                if (range.contains(x, y)) {
                    continue;
                }

                if (x < range.x) {
                    range.width += range.x - x;
                    range.x = x;
                }
                if (y < range.y) {
                    range.height += range.y - y;
                    range.y = y;
                }
                if (x >= range.x + range.width) {
                    range.width = x - range.x + 1;
                }
                if (y >= range.y + range.height) {
                    range.height = y - range.y + 1;
                }
            }
        }

        return range;
    }

    public void save(FileChannel channel, ByteBuffer buffer) throws IOException {
        // name
        byte [] nameBytes = name.getBytes();
        buffer.putInt(nameBytes.length);
        buffer.put(nameBytes);

        // get elements range
        Rectangle range = calcElementsRange();

        // range
        buffer.putInt(range.x);
        buffer.putInt(range.y);
        buffer.putInt(range.width);
        buffer.putInt(range.height);

        buffer.flip();
        channel.write(buffer);
        buffer.clear();

        // elements
        for (int x = 0; x < range.width; ++x) {
            for (int y = 0; y < range.height; ++y) {
                ChipElement elem = elements[range.x + x][range.y + y];

                if (buffer.position() > buffer.capacity() / 2) {
                    buffer.flip();
                    channel.write(buffer);
                    buffer.clear();
                }

                if (elem == null) {
                    buffer.putInt(ChipElement.ELEMENT_UNKNOWN);
                    continue;
                }

                elem.save(buffer);
            }
        }

        // models
        buffer.putInt(models.size());

        for (ChipModel mdl : models) {
            mdl.save(channel, buffer);
        }
    }

    public void clear() {
        for (ChipElement [] elemArray : elements) {
            for (int k = 0; k < elemArray.length; ++k) {
                elemArray[k] = null;
            }
        }

        for (int k = 0; k < inputs.size(); ++k) {
            inputs.set(k, false);
        }
        for (int k = 0; k < outputs.size(); ++k) {
            outputs.set(k, false);
        }

        models.clear();
    }

    public void load(int version, InputStream stream, ByteBuffer buffer) throws IOException {
        // name
        int res = stream.read(buffer.array(), 0, 4);
        assert res == 4;

        int nameLength = buffer.getInt(0);
        res = stream.read(buffer.array(), 0, nameLength);
        assert res == nameLength;

        buffer.position(0);
        buffer.limit(nameLength);
        name = Charset.defaultCharset().decode(buffer).toString();

        buffer.clear();

        // clear elements
        clear();

        res = stream.read(buffer.array(), 0, 16);
        assert res == 16;

        // range
        Rectangle range = new Rectangle();
        range.x = buffer.getInt(0);
        range.y = buffer.getInt(4);
        range.width  = buffer.getInt(8);
        range.height = buffer.getInt(12);

        // put elements to the center of diagram
        int offsetX = (DEFAULT_MODEL_SIZE - range.width) / 2;
        int offsetY = (DEFAULT_MODEL_SIZE - range.height) / 2;

        // elements
        for (int x = 0; x < range.width; ++x) {
            for (int y = 0; y < range.height; ++y) {
                ChipElement elem = ChipElement.load(stream, buffer);
                if (elem == null) {
                    continue;
                }

                elements[x + offsetX][y + offsetY] = elem;

                // update inputs & outputs arrays
                if (elem.type == ChipElement.ELEMENT_INPUT) {
                    while (inputs.size() < elem.itemType) {
                        inputs.add(false);
                    }
                    inputs.set(elem.itemType - 1, true);
                }
                if (elem.type == ChipElement.ELEMENT_OUTPUT) {
                    while (outputs.size() < elem.itemType) {
                        outputs.add(false);
                    }
                    outputs.set(elem.itemType - 1, true);
                }
            }
        }

        // models
        if (version == ModelPackage.OLD_VERSION_0_1) {
            return ;
        }

        buffer.clear();
        res = stream.read(buffer.array(), 0, 4);
        assert res == 4;

        int modelsCount = buffer.getInt(0);

        for (int k = 0; k < modelsCount; ++k) {
            ChipModel mdl = new ChipModel("");
            mdl.load(version, stream, buffer);

            models.add(mdl);
        }
    }

    public int getInputsCount() {
        int maxInput = 0;
        int maxOutput = 0;
        for (int k = inputs.size() - 1; k >= 0; --k) {
            if (inputs.get(k)) {
                maxInput = k;
                break;
            }
        }

        for (int k = outputs.size() - 1; k >= 0; --k) {
            if (outputs.get(k)) {
                maxOutput = k;
                break;
            }
        }

        return Math.max(maxInput + 1, maxOutput + 1);
    }
}
